package kit.persistency;

import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;

import kit.core.iEntropyCalculator;
import kit.main.Main;

import org.sqlite.SQLiteConfig;

/**
 * La persistenza dei dati viene garantita attraverso un database SQLlite. I
 * metodi di classe definiscono la struttura dei dati e popolano il db creato
 * dinamicamente degli elementi appartenenti al dominio; il dominio è definito
 * dal file "dominio.txt" e contiene coppie <Cognome_Italiano, Frequenza>.
 * 
 * @author mattia
 */
public class KitDB {
	private Connection connection;
	private static String filePath;

	public KitDB(String filePath) {
		try {
			KitDB.filePath = filePath;
			setUpConnection(filePath);
		} catch (ClassNotFoundException | SQLException e) {
		}
	}

	public KitDB() {
		try {
			setUpConnection(KitDB.filePath);
		} catch (ClassNotFoundException | SQLException e) {
		}
	}

	/**
	 * Genera una connessione al db.
	 * 
	 * @param filePath
	 *            Path del database SQLite.
	 * @throws SQLException
	 * @throws ClassNotFoundException
	 */
	private void setUpConnection(String filePath) throws SQLException,
			ClassNotFoundException {
		Class.forName("org.sqlite.JDBC");
		SQLiteConfig config = new SQLiteConfig();
		config.enforceForeignKeys(true);
		this.connection = DriverManager.getConnection(
				"jdbc:sqlite:" + filePath, config.toProperties());
	}

	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		connection.close();
	}

	/**
	 * Definisce lo schema del db.
	 * 
	 * @throws SQLException
	 */
	public void dbDDL() throws SQLException {
		this.buildSecretsTable();
		this.buildDomainTable();
		this.buildAnswersTable();
	}

	/**
	 * segreto(testo [PK])
	 * 
	 * @throws SQLException
	 */
	private void buildSecretsTable() throws SQLException {
		Statement stat = getConnection().createStatement();
		stat.executeUpdate("create table segreto" + "("
				+ "testo varchar(256) primary key not null);");
	}

	/**
	 * dominio(testo [PK], frequenza)
	 * 
	 * @throws SQLException
	 */
	private void buildDomainTable() throws SQLException {
		Statement stat = getConnection().createStatement();
		stat.executeUpdate("create table dominio" + "("
				+ "testo varchar(256) primary key not null,"
				+ "frequenza integer not null" + ");");
	}

	/**
	 * risposta(testoS [PK,FK_segreto], testoR [PK,FK_dominio], esposta,
	 * entropia)
	 * 
	 * @throws SQLException
	 */
	private void buildAnswersTable() throws SQLException {
		Statement stat = getConnection().createStatement();
		stat.executeUpdate("create table risposta" + "("
				+ "testoS varchar(256) not null,"
				+ "testoR varchar(256) not null,"
				+ "esposta integer not null default 0,"
				+ "entropia double precision not null,"
				+ "primary key(testoS, testoR),"
				+ "foreign key(testoS) references segreto(testo),"
				+ "foreign key(testoR) references dominio(testo)" + ");");
	}

	/**
	 * Inserimento di un nuovo segreto.
	 * 
	 * @param text
	 *            Testo del nuovo segreto.
	 * @return Vero se l'operazione va a buon fine, falso altrimenti.
	 */
	public boolean insertSecret(String text) {
		try {
			Connection conn = getConnection();
			Statement stat = conn.createStatement();
			stat.executeUpdate(String.format(
					"insert into segreto values (\"%s\");", text));
			stat.close();
		} catch (SQLException sql) {
			return false;
		}
		return true;
	}

	/**
	 * Inserimento di un nuovo elemento del dominio.
	 * 
	 * @param text
	 *            Testo dell'elemento del dominio.
	 * @param frequency
	 *            Frequenza assoluta.
	 * @return Vero se l'operazione va a buon fine, falso altrimenti.
	 */
	public boolean insertDomainElement(String text, int frequency) {
		try {
			Connection conn = getConnection();
			Statement stat = conn.createStatement();
			stat.executeUpdate(String.format(
					"insert into dominio(testo,frequenza)"
							+ " values (\"%s\",\"%s\");", text,
					Integer.toString(frequency)));
			stat.close();
		} catch (SQLException sql) {
			sql.printStackTrace();
			return false;
		}
		return true;
	}

	/**
	 * Inserimento di una risposta ad una domanda.
	 * 
	 * @param secret
	 *            Testo del segreto.
	 * @param answer
	 *            Testo della risposta.
	 * @return Vero se l'operazione va a buon fine, falso altrimenti.
	 */
	public boolean insertAnswer(String secret, String answer) {
		try {
			Connection conn = getConnection();
			Statement stat = conn.createStatement();
			double entropy = this.calculateOriginalEntropy(answer);
			String insert = String.format(
					"insert into risposta(testoS,testoR, entropia)"
							+ " values (\"%s\",\"%s\", %s);", secret, answer,
					entropy);
			stat.executeUpdate(insert);
			stat.close();
		} catch (SQLException sql) {
			return false;
		}
		return true;
	}
	
	/**
	 * @param question
	 *            Testo del segreto.
	 * @param answer
	 *            Testo della risposta.
	 * @param isExposed
	 *            Flag relativo all'esposizione.
	 * @param eCalc
	 *            Istanza della classe {@link iEntropyCalculator} per il
	 *            ridimensionamento del valore dell'entropia.
	 * @return Vero se l'operazione va a buon fine, falso altrimenti.
	 */
	public boolean setAnswerExposed(String question, String answer,
			boolean isExposed, iEntropyCalculator eCalc) {
		try {
			Connection conn = getConnection();
			Statement stat = conn.createStatement();
			String exposed = isExposed ? "1" : "0";
			Double entropy = isExposed ? eCalc.calculateEntropy(this
					.getAnswerEntropy(question, answer)) : this
					.calculateOriginalEntropy(answer);
			stat.executeUpdate(String
					.format("update risposta set esposta = %s, entropia = %s where testoS = \"%s\" and testoR = \"%s\";",
							exposed, entropy, question, answer));
			stat.close();
		} catch (SQLException sql) {
			return false;
		}
		return true;
	}

	/**
	 * Verifica se la risposta ad un particolare segreto è stato esposto.
	 * 
	 * @param secret
	 *            Testo del segreto.
	 * @param answer
	 *            Testo della risposta.
	 * @return Vero se la coppia <segreto,risposta> è stata esposta, falso
	 *         altrimenti.
	 */
	public boolean isAnswerExposed(String secret, String answer) {
		try {
			Connection conn = getConnection();
			Statement stat = conn.createStatement();
			ResultSet rs = stat
					.executeQuery(String
							.format("select * from risposta where testoS = \"%s\" and testoR = \"%s\";",
									secret, answer));
			if (!rs.next())
				return false;
			boolean result = rs.getInt("esposta") == 1;
			rs.close();
			return result;
		} catch (SQLException sql) {
			return false;
		}
	}

	/**
	 * @param secret
	 *            Testo del segreto.
	 * @param answer
	 *            Testo della risposta.
	 * @return Valore d'entropia per la coppia <segreto, risposta>
	 */
	public double getAnswerEntropy(String secret, String answer) {
		try {
			Statement stat = connection.createStatement();
			ResultSet rs = stat
					.executeQuery(String
							.format("select entropia from risposta where testoS = \"%s\" and testoR = \"%s\";",
									secret, answer));
			if (!rs.next())
				return -1;
			double entropia = rs.getDouble("entropia");
			rs.close();
			stat.close();
			return entropia;
		} catch (SQLException e) {
			return -1;
		}
	}

	/**
	 * Calcola l'entropia di una data risposta secondo le frequenze originali
	 * previste dal dominio.
	 * 
	 * @param answer
	 *            Testo della risposta.
	 * @return
	 */
	private double calculateOriginalEntropy(String answer) {
		try {
			Statement stat = connection.createStatement();
			ResultSet rs = stat
					.executeQuery("select * from dominio order by testo;");
			int answerFrequency = 0, domainTotalFrequency = 0;
			while (rs.next()) {
				if (rs.getString("testo").equals(answer)) {
					answerFrequency = rs.getInt("frequenza");
				}
				domainTotalFrequency += rs.getInt("frequenza");
			}
			double prob = (answerFrequency * 1.0) / domainTotalFrequency;
			//double entropy = -1.0 * prob * (Math.log(prob) / Math.log(2));
			double entropy = -1.0 * (Math.log(prob) / Math.log(2));
			rs.close();
			stat.close();
			return entropy;
		} catch (SQLException e) {
			return -1;
		}
	}

	/**
	 * @return Lista dei segreti presenti sul db.
	 */
	public List<String> getSecrets() {
		LinkedList<String> domainElements = new LinkedList<>();
		try {
			Statement stat = connection.createStatement();
			ResultSet rs = stat
					.executeQuery("select testo from segreto order by testo;");
			while (rs.next())
				domainElements.add(rs.getString("testo"));
			rs.close();
			return domainElements;
		} catch (SQLException e) {
			return new LinkedList<String>();
		}
	}

	/**
	 * @param secret
	 *            Testo del segreto.
	 * @return Lista delle risposte associate ad un dato segreto.
	 */
	public List<String> getAnswers(String secret) {
		LinkedList<String> domainElements = new LinkedList<>();
		try {
			Statement stat = connection.createStatement();
			ResultSet rs = stat
					.executeQuery(String
							.format("select testoR from risposta where testoS = \"%s\" order by testoR;",
									secret));
			while (rs.next())
				domainElements.add(rs.getString("testoR"));
			rs.close();
			return domainElements;
		} catch (SQLException e) {
			return new LinkedList<String>();
		}
	}

	/**
	 * @return Lista degli elementi che costituiscono il dominio.
	 */
	public List<String> getDomainElements() {
		LinkedList<String> domainElements = new LinkedList<>();
		try {
			Statement stat = connection.createStatement();
			ResultSet rs = stat
					.executeQuery("select testo from dominio order by testo;");
			while (rs.next())
				domainElements.add(rs.getString("testo"));
			rs.close();
			return domainElements;
		} catch (SQLException e) {
			return new LinkedList<String>();
		}
	}

	/**
	 * Inizializzazione del db, popola la tabella "dominio" ottenendo i valori
	 * dal file "domain.txt"
	 * 
	 * @return Vero se l'operazione va a buon fine, falso altrimenti.
	 * @throws IOException
	 */
	public boolean insertDomainElement() throws IOException {
		InputStream stream = Main.class.getResourceAsStream("domain.txt");

		if (stream == null) {
			return false;
		}

		Scanner input = new Scanner(stream);
		boolean result = true;
		while (input.hasNextLine()) {
			String line = input.nextLine();
			String[] domainItem = line.split(" ");
			result = insertDomainElement(domainItem[0],
					Integer.parseInt(domainItem[1]))
					&& result;
			if (!result) {
				break;
			}
		}

		input.close();
		return result;
	}

	public Connection getConnection() throws SQLException {
		if (connection == null)
			throw new SQLException("Connessione non stabilita");
		return connection;
	}

	public static String DBName = "kit.db";
}
